package web.common.internal.core.io;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.common.core.io.FileMetadata;
import web.common.core.io.StorageException;
import web.common.core.io.StorageOptions;
import web.common.core.io.StorageService;
import web.common.core.util.AssertUtil;
import web.common.core.util.ByteUtil;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @author Jin Zheng
 * @since 1.0 2021-11-15
 */
public class LocalStorageService implements StorageService {
    private static Logger logger = LoggerFactory.getLogger(LocalStorageService.class);
    private static final DeleteDirectory DELETE_DIR = new DeleteDirectory();
    private static final int BUF_SIZE = 4096;

    private final StorageOptions options;

    public LocalStorageService(StorageOptions options) {
        this.options = options;
    }

    @Override
    public FileMetadata save(String path, InputStream is) throws StorageException {
        var dest = this.buildPath(path, true);
        var md = this.getMessageDigest();

        var buf = new byte[BUF_SIZE];
        int read = 0;
        long size = 0L;
        try (is;
            var os = Files.newOutputStream(dest)) {
            while ((read = is.read(buf)) != -1) {
                if (md != null) {
                    md.update(buf, 0, read);
                }
                os.write(buf, 0, read);
                os.flush();
                size += read;
            }
            var metadata = new DefaultFileMetadata();
            metadata.setFilename(dest.getFileName().toString());
            metadata.setSize(size);
            metadata.setHashType(options.getHashType());
            if (md != null) {
                var hash = md.digest();
                metadata.setHash(ByteUtil.toHexString(hash));
            }

            logger.info("Save file [{}] {} bytes, hash [{}]: {}", dest, size, options.getHashType(), metadata.getHash());
            return metadata;
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    private MessageDigest getMessageDigest() {
        if (options.getHashType() == null || options.getHashType().isEmpty()) {
            return null;
        }

        try {
            return MessageDigest.getInstance(options.getHashType());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public void copy(String srcPath, String destPath) throws StorageException {
        var src = this.buildPath(srcPath, false);
        var dest = this.buildPath(destPath, true);
        try {
            Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
            logger.info("Copy file [{}] to [{}]", src, dest);
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    @Override
    public void move(String srcPath, String destPath) throws StorageException {
        var src = this.buildPath(srcPath, false);
        var dest = this.buildPath(destPath, true);
        try {
            Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
            logger.info("Move file [{}] to [{}]", src, dest);
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    @Override
    public void remove(String path) throws StorageException {
        var targetPath = this.buildPath(path, false);
        if (!Files.exists(targetPath)) {
            logger.warn("File not found: {}", targetPath);
            return;
        }

        try {
            if (Files.isDirectory(targetPath)) {
                Files.walkFileTree(targetPath, DELETE_DIR);
            } else {
                var success = Files.deleteIfExists(targetPath);
                logger.info("Remove file [{}] {}", targetPath, success);
            }
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    @Override
    public InputStream read(String path) throws StorageException {
        AssertUtil.notEmpty(path, "Path");
        var targetPath = this.buildPath(path, false);
        try {
            return Files.newInputStream(targetPath);
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    @Override
    public boolean exists(String path) throws StorageException {
        var targetPath = this.buildPath(path, false);
        return Files.exists(targetPath);
    }

    @Override
    public FileMetadata writeString(String path, String content) throws StorageException {
        var targetPath = this.buildPath(path, true);
        try {
            Files.writeString(targetPath, content);
            var metadata = new DefaultFileMetadata();
            metadata.setFilename(targetPath.getFileName().toString());
            metadata.setSize(Files.size(targetPath));
            metadata.setHashType(options.getHashType());
            metadata.setHash(this.hashFile(targetPath));

            logger.info("Save file [{}] {} bytes, hash [{}]: {}", targetPath, metadata.getSize(), options.getHashType(), metadata.getHash());
            return metadata;
        } catch (IOException e) {
            throw new StorageException(e);
        }
    }

    private String hashFile(Path path) throws IOException {
        var md = this.getMessageDigest();
        if (md == null) {
            return null;
        }
        var buf = new byte[BUF_SIZE];
        int read = -1;
        try (var is = Files.newInputStream(path)) {
            while ((read = is.read(buf)) != -1) {
                md.update(buf, 0, read);
            }
            byte[] hash = md.digest();
            return ByteUtil.toHexString(hash);
        }
    }

    private Path buildPath(String path, boolean isCreateDir) {
        AssertUtil.notEmpty(path, "Path");
        var targetPath = Paths.get(options.getRootPath(), path);
        var dir = targetPath.getParent();
        if (isCreateDir && !Files.exists(dir)) {
            try {
                Files.createDirectories(dir);
                logger.info("Create Directory: {}", dir);
            } catch (IOException e) {
                throw new StorageException(e);
            }
        }
        return targetPath;
    }

}
